iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
Modern Web

Hello TypeScript 菜鳥系列 第 21

Day 20. TypeScript Generic 泛型:Generic Constraints

  • 分享至 

  • xImage
  •  

題外話一下,這個系列有時候如果發現有錯誤或是有新知識就會回去相關文章補充。

這個系列的第一篇generic文章的最後有補充了一個例子:

function getLength<T>(args: T): number {
	return args.length;	// compile error
}

那篇有解釋:因為TypeScript Compiler無法保證輸入的型別變數 T 至少有一個屬性稱作 length而報錯,那時解決的方式是讓 T 是個會有 length 屬性的generic array,所以這邊的 T 可以寫成 T[] 或是 Array<T>

而這種改寫成generic array的情況可以說是對generic的型別加上一點限制,官方文件將這種限制用法稱為「generic constraint」。

但是這樣會有一個問題是,若是我傳入的型別不是array,而是也有 length 屬性的其他型別呢?

因此今天的文章就要來多說一點generic constraints來解決上面這個問題。


Structural type system

繼續往下講generic constraints之前,要先暫停一下來認識TypeScript型別系統的特性。

TypeScript的型別系統是屬於結構化型別系統(structural type system),這種型別系統辨識型別的方法是:

「只要某個型別的樣子長得像我們需要的型別樣子就可以了。」

採用這種方式決定型別的方法稱為結構定型(structure typing)或是鴨子定型(duck typing)。

換句話說,假設我們指定某個函式輸入參數的型別要有幾個特定名稱的屬性和方法,只要某個引數的型別有這幾個指定的屬性和方法,即使這個引數的型別還有其他的屬性和方法,在TypeScript眼裡,這個引數的型別已經有一部份 長得像 指定的參數型別,那麼輸入這個引數作為函式參數就不會報錯。


Generic constraints

interface的介紹文章有提過一個很好用的關鍵字 ─ extends,文章有稍微介紹 extends 可以延伸、重複利用其他interface所擁有的屬性或方法。

所以根據前述structural type system的特性,我們就讓generic的型別變數 extendslength 屬性的interface,使得輸入型別變數可以是任何有 length 屬性的型別就能解決前面的問題了。

為了不要讓 extends 有其他不必要的屬性和方法,這邊自訂一個只有 length 屬性的interface,然後改寫函式讓型別變數 extends 這個interface:

interface LengthProp{
	length: number;
}

function getLength<T extends LengthProp>(args: T): number {
	return args.length;
}


let numArr = [0, 1, 2, 3];

console.log(getLength(numArr));	// 4

某些時候,也有可能要讓某個型別變數去限制其他的型別變數,這邊以官方handbook的例子為例,假設今天要取得某個物件的屬性值,為了確保輸入的屬性名存在於物件中,可以這麼做:


function getProperty<T, K extends keyof T>(obj: T, key: K) {
	return obj[key];
}


// 驗證
const person = {
	id: 12345678,
	first_name: "Clark",
	last_name: "Kent",
	birth_year: 1938,
}

console.log(getProperty(person, "id"));		// 12345678
console.log(getProperty(person, "name"));	// undefined

這個範例裡的 keyof 是TypeScript的運算子。在TypeScript語法中,keyof 可以將一個物件的屬性名稱視為一種特殊型別(literal types),而 keyof 關鍵字就是取得一個型別的所有屬性當作一個 literal union type,譬如:

// keyof example

const person = {
	id: 12345678,
	first_name: "Clark",
	last_name: "Kent",
	birth_year: 1938,
}

type PERSON = typeof person;	// 先取得物件型別
/*  
 *  PERSON = {
 *     id: number,
 *     first_name: string,
 *     last_name: string,
 *     birth_year: number,
 *  }
 */

type P= keyof PERSON;	// "id" | "first_name" | "last_name" | "birth_year"

型別 P 的取得方式也可以一步驟寫成 keyof typeof person

今天主要內容大致到這裡,從以上例子可以看到TypeScript可能還有很多關鍵字(keyword)或是運算子(operator)是前面文章沒有特別提到的,接下來預計會有幾篇文章來探討TypeScript的關鍵字和運算子。


後記

補充一個有趣的例子,假設今天希望 getLength 函式也能計算物件(object)擁有的屬性和方法數量,改寫一下前面函式,讓型別變數變成 union 型別變數,使得輸入參數可以是object:

interface LengthProp {
	length: number;
}

const has = <T, K extends keyof T> (arg: T, prop: K) =>{
	return arg[prop];
}

function getLength<T extends LengthProp>(arg: T | object): number | never {
	if (typeof arg === 'object'){
		return Object.keys(arg).length;
	} else if (has(arg, 'length')){
		return (arg as T).length;	// as 斷言 arg 型別一定是T
	} else{
		throw new Error("Unknown type!");
	}
}


let numArr = [0, 1, 2, 3];
let strObj = {
	hello: 'Hello',
	world: 'World'
}
let x = 1

console.log(getLength(numArr));	// 4
console.log(getLength(strObj));	// 2
console.log(getLength(x));		// Error: Unknown type! 

參考資料
Generic @TypeScript Handbook
TypeScript
TypeScript for JavaScript Programmers @TypeScript
Structural type system
keyof @TypeScript Handbook
literal types @TypeScript Handbook


上一篇
Day 19. TypeScript Generic 泛型:Generic Interface、Generic Class
下一篇
Day 21. TypeScript Type What?:Type Alias
系列文
Hello TypeScript 菜鳥31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言